package slidingpenguins.utils.imagecache {
	import flash.events.ErrorEvent;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.ProgressEvent;
	import flash.events.SecurityErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import flash.utils.ByteArray;
	import flash.utils.Dictionary;
	
	import mx.collections.ArrayList;
	import mx.collections.IList;
	import mx.controls.Image;

	public class ImageCache {
		public static const DEFAULT_SIZE:uint = 100 * 1024 * 1024;
		private static const MAX_PREFETCH_TIME:uint = 60000;
		
		private var cache:Dictionary;
		private var priorityList:IList;
		private var _size:uint;
		private var _maxSize:uint;
		// private var useTempStorage:Boolean; to be implemented

		public function ImageCache(maxSize:uint = ImageCache.DEFAULT_SIZE) {
			cache = new Dictionary();
			priorityList = new ArrayList(); // no LinkedList/PriorityQueue in Flex :(
			_size = 0;
			_maxSize = maxSize;
		}
		
		public function get size():uint {
			return _size;
		}
		
		public function get maxSize():uint {
			return _maxSize;
		}
		
		public function set maxSize(maxSize:uint):void {
			_maxSize = maxSize;
			
			checkCacheSize();
		}
		
		public function removeHolesInThePriorityList():void {
			var currentEmptyIndex:uint = 0;
			var currentNonEmptyIndex:uint = 0;

			while (currentNonEmptyIndex < priorityList.length) {
				while (currentEmptyIndex < priorityList.length && priorityList.getItemAt(currentEmptyIndex) != null) {
					currentEmptyIndex++;
				}

				currentNonEmptyIndex = currentEmptyIndex;
				
				while(currentNonEmptyIndex < priorityList.length && priorityList.getItemAt(currentNonEmptyIndex) == null) {
					currentNonEmptyIndex++;
				}
				
				while (currentNonEmptyIndex < priorityList.length && priorityList.getItemAt(currentNonEmptyIndex) != null) {
					priorityList.setItemAt(priorityList.getItemAt(currentNonEmptyIndex), currentEmptyIndex);
					priorityList.setItemAt(null, currentNonEmptyIndex);

					currentEmptyIndex++;
					currentNonEmptyIndex++;
				}
			}
			
			while (currentEmptyIndex < priorityList.length) {
				priorityList.removeItemAt(priorityList.length - 1);
			}
		}
		
		private function deleteEntry(i:uint):void {
			var entry:ImageCacheEntry = getCacheEntry(priorityList.getItemAt(i) as String);

			delete cache[priorityList.getItemAt(i)];
			priorityList.setItemAt(null, i);
			entry.aliasesCount--;
			
			if (entry.cachedImage.isReady && entry.aliasesCount == 0) {
				_size -= entry.cachedImage.bytes.length;
			}
		}
		
		private function freeEntries(skipRecentPrefetches:Boolean):void {
			for (var i:uint = 0; i < priorityList.length && size > maxSize; i++) {
				if (priorityList.getItemAt(i) == null) {
					continue;
				}
				
				var entry:ImageCacheEntry = getCacheEntry(priorityList.getItemAt(i) as String);
				
				if (skipRecentPrefetches && entry.isInPrefetchedAndNotAccessedState &&
					entry.prefetchedRequestTime + MAX_PREFETCH_TIME >= (new Date()).time) {
					continue;
				}
				
				if (!entry.isUndeletable && entry.cachedImage.isReady) {
					deleteEntry(i);
				}
			}
		}
		
		private function checkCacheSize():void {
			if (size > maxSize) {
				freeEntries(true);
				freeEntries(false);
				
				removeHolesInThePriorityList();
			}
		}
		
		private function getCacheEntry(url:String):ImageCacheEntry {
			return cache[url] as ImageCacheEntry;
		}
		
		private function getPriorityIndex(url:String):uint {
			for (var i:uint = priorityList.length - 1; i >= 0; i--) {
				if (priorityList.getItemAt(i) == null) {
					continue;
				}

				if (priorityList.getItemAt(i) == url) {
					return i;
				}
			}
			
			return priorityList.length;
		}
		
		private function getLastInPrefetchedAndNotAccessedStateEntry():uint {
			for (var i:uint = 0; i < priorityList.length; i++) {
				if (priorityList.getItemAt(i) == null) {
					continue;
				}

				if (getCacheEntry(priorityList.getItemAt(i) as String).isInPrefetchedAndNotAccessedState) {
					return i;
				}
			}
			
			return priorityList.length;
		}
		
		private function addEntry(url:String, entry:ImageCacheEntry):void {
			cache[url] = entry;
			
			if (entry.isInPrefetchedAndNotAccessedState) {
				priorityList.addItemAt(url, getLastInPrefetchedAndNotAccessedStateEntry());
			} else {
				priorityList.addItem(url);
			}
		}
		
		private function getImageCompleteHandler(urlLoader:URLLoader, url:String, cachedImage:MutableCachedImage):Function {
			return function(event:Event):void {
				cachedImage.bytes = urlLoader.data as ByteArray;
				cachedImage.isReady = true;
				cachedImage.dispatchEvent(new Event(Event.COMPLETE));
				
				if (cache[url]) {
					_size += urlLoader.bytesTotal;
					
					checkCacheSize();
				}
			};
		}
		
		private function getImageProgressHandler(cachedImage:CachedImage):Function {			
			return function(event:ProgressEvent):void {
				cachedImage.dispatchEvent(event);
			};
		}
		
		private function getImageErrorHandler(cachedImage:MutableCachedImage):Function {
			return function(event:ErrorEvent):void {
				cachedImage.isInErrorState = true;
				cachedImage.dispatchEvent(event);
			};
		}
		
		private function createAndExecuteURLLoaderForEntry(url:String, entry:ImageCacheEntry):void {
			entry.cachedImage.isInErrorState = false;
			
			var urlRequest:URLRequest = new URLRequest(url);
			var urlLoader:URLLoader = new URLLoader();
			urlLoader.dataFormat = URLLoaderDataFormat.BINARY;
			urlLoader.addEventListener(Event.COMPLETE, getImageCompleteHandler(urlLoader, url, entry.cachedImage));
			urlLoader.addEventListener(ProgressEvent.PROGRESS, getImageProgressHandler(entry.cachedImage));
			urlLoader.addEventListener(IOErrorEvent.IO_ERROR, getImageErrorHandler(entry.cachedImage));
			urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, getImageErrorHandler(entry.cachedImage));
			urlLoader.load(urlRequest);
		}
		
		private function createURLLoaderEntry(url:String):ImageCacheEntry {
			var entry:ImageCacheEntry = new ImageCacheEntry(new MutableCachedImage());
			
			createAndExecuteURLLoaderForEntry(url, entry);
			
			return entry;
		}
		
		public function prefetchImage(url:String, makeUndeletable:Boolean = false):void {
			var entry:ImageCacheEntry

			if (cache[url]) {
				entry = getCacheEntry(url);
				
				if (!entry.isInPrefetchedAndNotAccessedState) {
					var currentPriority:int = getPriorityIndex(url);
					var lastPrefetchedPriority:int = getLastInPrefetchedAndNotAccessedStateEntry();
					
					if (currentPriority < lastPrefetchedPriority) {
						priorityList.setItemAt(null, currentPriority);
						priorityList.addItemAt(url, lastPrefetchedPriority);
					}
				}
				
				if (entry.cachedImage.isInErrorState) {
					createAndExecuteURLLoaderForEntry(url, entry);
				}
			} else {
				entry = createURLLoaderEntry(url);
			}
			
			entry.isInPrefetchedAndNotAccessedState = true;
			entry.prefetchedRequestTime = new Date().time;
			if (!entry.isUndeletable) {
				entry.isUndeletable = makeUndeletable;
			}
			
			if (!cache[url]) {
				addEntry(url, entry);				
			}
		}
		
		public function getImage(url:String, makeUndeletable:Boolean = false):CachedImage {
			var entry:ImageCacheEntry

			if (cache[url]) {
				entry = getCacheEntry(url);
				
				if (entry.isInPrefetchedAndNotAccessedState) {
					entry.isInPrefetchedAndNotAccessedState = false;
					entry.prefetchedRequestTime = 0;
				}

				return entry.cachedImage;
				
				if (entry.cachedImage.isInErrorState) {
					createAndExecuteURLLoaderForEntry(url, entry);
				}
			} else {
				entry = createURLLoaderEntry(url);
			}

			if (!entry.isUndeletable) {
				entry.isUndeletable = makeUndeletable;
			}
			
			if (!cache[url]) {
				addEntry(url, entry);				
			}

			return entry.cachedImage;
		}
		
		public function addImage(url:String, bytes:ByteArray, makeUndeletable:Boolean = false):void {
			if (cache[url]) {
				throw new ArgumentError("Image already in cache: " + url);
			}
			
			var entry:ImageCacheEntry = new ImageCacheEntry(new MutableCachedImage());
			entry.cachedImage.bytes = bytes;
			entry.cachedImage.isReady = true;
			entry.isUndeletable = makeUndeletable;
			
			addEntry(url, entry);
			
			_size += entry.cachedImage.bytes.length;
			checkCacheSize();
		}
		
		public function contains(url:String):Boolean {
			if (cache[url]) {
				return true;
			}
			
			return false;
		}
		
		public function createAlias(url:String, alias:String):void {
			if (!cache[url]) {
				throw new ArgumentError("Image not in cache: " + url);
			}
			deleteImage(alias);
			
			var entry:ImageCacheEntry = getCacheEntry(url);
			entry.aliasesCount++;
			
			addEntry(alias, entry);			
		}
		
		public function makeDeletable(url:String):void {
			if (cache[url]) {
				getCacheEntry(url).isUndeletable = false;
			}
		}
		
		public function deleteImage(url:String):void {
			if (cache[url]) {
				deleteEntry(priorityList.getItemIndex(url));
			}
		}
	}
}
